Micron Document
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| SparkN0de-git | SparkN0de |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------


Commit ca05f4d7705cd4d32e225aeac9e02165424510ca


Parents : b7fa09d
Author : Ivan <ivan@quad4.io>
Signature : Invalid signer <e46112d44649266d71fe2193e00a4710>, author is <ivan@quad4.io>
Date : 2026-06-16T18:50:26-05:00

feat(pathfinder): add pathfinding functionality to the frontend and backend, including quick, force, and drop requests, along with a new API endpoint for clearing the path table

Changes
Diff

diff --git a/meshchatx/src/backend/rnpath_handler.py b/meshchatx/src/backend/rnpath_handler.py
index 5ce208ee..6caa5bf0 100644
--- a/meshchatx/src/backend/rnpath_handler.py
+++ b/meshchatx/src/backend/rnpath_handler.py
@@ -121,6 +121,20 @@ class RNPathHandler:
except Exception:
return False
+ def drop_all_paths(self) -> int:
+ dropped = 0
+ try:
+ table = self.reticulum.get_path_table()
+ for entry in table:
+ try:
+ if self.reticulum.drop_path(entry["hash"]):
+ dropped += 1
+ except Exception:
+ continue
+ except Exception:
+ return dropped
+ return dropped
+
def drop_all_via(self, transport_instance_hash: str) -> bool:
try:
ti_bytes = bytes.fromhex(transport_instance_hash)

diff --git a/meshchatx/src/frontend/components/messages/ConversationPeerHeader.vue b/meshchatx/src/frontend/components/messages/ConversationPeerHeader.vue
index f2c6cbf9..4ac6c0dd 100644
--- a/meshchatx/src/frontend/components/messages/ConversationPeerHeader.vue
+++ b/meshchatx/src/frontend/components/messages/ConversationPeerHeader.vue
@@ -114,6 +114,35 @@
</div>
<div class="ml-auto flex items-center gap-0.5 sm:gap-1.5 min-w-0 shrink-0">
+ <DropDownMenu class="shrink-0">
+ <template #button>
+ <IconButton
+ :title="$t('nomadnet.path_finder')"
+ :disabled="pathfinderInProgress"
+ class="text-blue-600 dark:text-blue-400"
+ >
+ <MaterialDesignIcon
+ :icon-name="pathfinderInProgress ? 'loading' : 'map-marker-path'"
+ :class="['size-6 sm:size-7', pathfinderInProgress ? 'animate-spin' : '']"
+ />
+ </IconButton>
+ </template>
+ <template #items>
+ <DropDownMenuItem @click="$emit('path-finder-quick')">
+ <MaterialDesignIcon icon-name="flash" class="size-5" />
+ <span>{{ $t("nomadnet.path_finder_quick_request") }}</span>
+ </DropDownMenuItem>
+ <DropDownMenuItem @click="$emit('path-finder-force')">
+ <MaterialDesignIcon icon-name="map-marker-radius" class="size-5" />
+ <span>{{ $t("nomadnet.path_finder_force_find") }}</span>
+ </DropDownMenuItem>
+ <DropDownMenuItem @click="$emit('path-finder-drop')">
+ <MaterialDesignIcon icon-name="reload-alert" class="size-5" />
+ <span>{{ $t("nomadnet.path_finder_drop_and_request") }}</span>
+ </DropDownMenuItem>
+ </template>
+ </DropDownMenu>
+
<ConversationDropDownMenu
v-if="selectedPeer"
:peer="selectedPeer"
@@ -143,6 +172,8 @@ import MaterialDesignIcon from "../MaterialDesignIcon.vue";
import IconButton from "../IconButton.vue";
import LxmfUserIcon from "../LxmfUserIcon.vue";
import ConversationDropDownMenu from "./ConversationDropDownMenu.vue";
+import DropDownMenu from "../DropDownMenu.vue";
+import DropDownMenuItem from "../DropDownMenuItem.vue";
dayjs.extend(relativeTime);
@@ -153,6 +184,8 @@ export default {
IconButton,
LxmfUserIcon,
ConversationDropDownMenu,
+ DropDownMenu,
+ DropDownMenuItem,
},
props: {
selectedPeer: {
@@ -183,6 +216,10 @@ export default {
type: Object,
default: null,
},
+ pathfinderInProgress: {
+ type: Boolean,
+ default: false,
+ },
},
emits: [
"edit-display-name",
@@ -197,6 +234,9 @@ export default {
"start-call",
"share-contact",
"close",
+ "path-finder-quick",
+ "path-finder-force",
+ "path-finder-drop",
],
computed: {
destinationDisplay() {

diff --git a/meshchatx/src/frontend/components/messages/ConversationViewer.vue b/meshchatx/src/frontend/components/messages/ConversationViewer.vue
index 563b0fc0..a4fb980f 100644
--- a/meshchatx/src/frontend/components/messages/ConversationViewer.vue
+++ b/meshchatx/src/frontend/components/messages/ConversationViewer.vue
@@ -28,11 +28,15 @@
:selected-peer-path="selectedPeerPath"
:selected-peer-signal-metrics="selectedPeerSignalMetrics"
:selected-peer-lxmf-stamp-info="selectedPeerLxmfStampInfo"
+ :pathfinder-in-progress="pathfinderInProgress"
@edit-display-name="updateCustomDisplayName"
@copy-hash="copyHash"
@destination-path-click="onDestinationPathClick"
@signal-metrics-click="onSignalMetricsClick"
@stamp-info-click="onStampInfoClick"
+ @path-finder-quick="runPathFinderQuickRequest"
+ @path-finder-force="runPathFinderForceFind"
+ @path-finder-drop="runPathFinderDropAndRequest"
@conversation-deleted="onConversationDeleted"
@popout="openConversationPopout"
@retry-failed="retryAllFailedOrCancelledMessages"
@@ -1728,7 +1732,7 @@ import ConversationMessageEntry from "./ConversationMessageEntry.vue";
import ConversationMessageListVirtual from "./ConversationMessageListVirtual.vue";
import { displayGroupsOldestFirst, MIN_VIRTUAL_DISPLAY_GROUPS } from "./messageListVirtual.js";
import DialogUtils from "../../js/DialogUtils";
-import { getDestinationPath, postRequestPath } from "../../js/reticulumPathfinding.js";
+import { getDestinationPath, postRequestPath, runDestinationPathFinder } from "../../js/reticulumPathfinding.js";
import MicrophoneRecorder from "../../js/MicrophoneRecorder";
import WebSocketConnection from "../../js/WebSocketConnection";
import AddAudioButton from "./AddAudioButton.vue";
@@ -1811,6 +1815,7 @@ export default {
selectedPeerPath: null,
selectedPeerLxmfStampInfo: null,
selectedPeerSignalMetrics: null,
+ pathfinderInProgress: false,
lxmfMessagesRequestSequence: 0,
chatItems: [],
@@ -4726,6 +4731,65 @@ export default {
console.log(e);
}
},
+ async runPathFinderQuickRequest() {
+ const hash = this.selectedPeer?.destination_hash;
+ if (!hash || this.pathfinderInProgress) {
+ return;
+ }
+ this.pathfinderInProgress = true;
+ try {
+ await runDestinationPathFinder(window.api, hash, "quick");
+ ToastUtils.success(this.$t("nomadnet.path_finder_request_sent"));
+ await this.getPeerPath();
+ } catch (e) {
+ console.error("path finder quick request failed", e);
+ ToastUtils.error(this.$t("nomadnet.path_finder_failed"));
+ } finally {
+ this.pathfinderInProgress = false;
+ }
+ },
+ async runPathFinderForceFind() {
+ const hash = this.selectedPeer?.destination_hash;
+ if (!hash || this.pathfinderInProgress) {
+ return;
+ }
+ this.pathfinderInProgress = true;
+ try {
+ const { path } = await runDestinationPathFinder(window.api, hash, "force", {
+ forceTimeout: 15,
+ });
+ if (path) {
+ ToastUtils.success(this.$t("nomadnet.path_finder_found"));
+ this.selectedPeerPath = path;
+ } else {
+ ToastUtils.error(this.$t("nomadnet.path_finder_not_found"));
+ }
+ } catch (e) {
+ console.error("path finder force find failed", e);
+ ToastUtils.error(this.$t("nomadnet.path_finder_failed"));
+ } finally {
+ this.pathfinderInProgress = false;
+ }
+ },
+ async runPathFinderDropAndRequest() {
+ const hash = this.selectedPeer?.destination_hash;
+ if (!hash || this.pathfinderInProgress) {
+ return;
+ }
+ this.pathfinderInProgress = true;
+ try {
+ await runDestinationPathFinder(window.api, hash, "drop_then_request", {
+ onDropPathError: (e) => console.warn("drop-path failed (continuing)", e),
+ });
+ ToastUtils.success(this.$t("nomadnet.path_finder_dropped_and_requested"));
+ await this.getPeerPath();
+ } catch (e) {
+ console.error("path finder drop+request failed", e);
+ ToastUtils.error(this.$t("nomadnet.path_finder_failed"));
+ } finally {
+ this.pathfinderInProgress = false;
+ }
+ },
imageGroupGalleryUrls(items) {
return this.imageGroupSortedChron(items).map((it) => this.lxmfImageUrl(it.lxmf_message.hash));
},

diff --git a/tests/backend/test_rnpath_logic.py b/tests/backend/test_rnpath_logic.py
index b2ae8c41..cda2e5f8 100644
--- a/tests/backend/test_rnpath_logic.py
+++ b/tests/backend/test_rnpath_logic.py
@@ -130,3 +130,35 @@ async def test_rnpath_drop_endpoint(mock_rns_minimal, temp_dir):
app_instance.reticulum.drop_path.assert_called_with(bytes.fromhex(target_hash))
assert json.loads(response.body)["success"] is True
+
+
+@pytest.mark.asyncio
+async def test_maintenance_clear_path_table(mock_rns_minimal, temp_dir):
+ with patch("meshchatx.meshchat.generate_ssl_certificate"):
+ app_instance = ReticulumMeshChat(
+ identity=mock_rns_minimal,
+ storage_dir=temp_dir,
+ reticulum_config_dir=temp_dir,
+ )
+
+ entry = {
+ "hash": b"\x01" * 32,
+ "hops": 1,
+ "via": b"\x02" * 32,
+ "interface": "UDP",
+ "expires": 1234567890,
+ }
+ app_instance.reticulum.get_path_table.return_value = [entry, entry]
+
+ request = MagicMock()
+ handler = next(
+ r.handler
+ for r in app_instance.get_routes()
+ if r.path == "/api/v1/maintenance/path-table"
+ )
+ response = await handler(request)
+ data = json.loads(response.body)
+
+ assert response.status == 200
+ assert data["dropped"] == 2
+ assert app_instance.reticulum.drop_path.call_count == 2


──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────